
//    LunchIT - Tool to make decisions of a lunch restaurant.
//    Copyright (C) 2010 Jukka Hell
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

package fi.hell.lunchit

import fi.hell.enums.Day
import fi.hell.enums.RestaurantSort
import javax.imageio.ImageIO
import java.awt.image.BufferedImage
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH

class Restaurant {

    static def dataSource
    static final long PACKED_LUNCH_ID = 18
	static hasMany = [openingDays:Day, tags:RestaurantTag]

	String name
	String homePage
	byte[] logo
    int distance
    String address
    String city
    String zip

    static constraints = {
		name		(blank:false)
		homePage	(url:true, nullable:true)
		address		(nullable:true)
		city		(nullable:true)
		zip			(nullable:true)
        tags	    (nullable:true)
		logo		(maxSize:1048576 /* 1mb */ , validator: { val, obj ->
			InputStream bais = new ByteArrayInputStream(val)
			BufferedImage image = ImageIO.read(bais)

			if (image?.getWidth() != (CH.config.restaurant.logo.width as int) || image?.getHeight() != (CH.config.restaurant.logo.height as int)) {
				return ['restaurant.invalid.logoDimensions', CH.config.restaurant.logo.width, CH.config.restaurant.logo.height]
			}
		})
    }

	String toString() {
		return name
	}

    /**
     * Is this restaurant open today
     */
	boolean openToday() {
		int today = Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
		// Sunday is number 1 in Calendar DAY_OF_WEED and in our DAY enum its ordinal is number 6
		int ordinal = ((today - 2) >= 0) ? today - 2 : 6
		Day todayEnum = Day.values()[ordinal]
		return openingDays.contains(todayEnum)
	}

    /**
     * Who has selected this restaurant the most
     */
	Map<User, Integer> heavyUser() {
		List results = Round.createCriteria().get {
			eq("selection", this)
			projections {
				groupProperty("user")
				count("selection", "selectionCount")
			}
			order("selectionCount", "desc")
			maxResults(1)
		}

		Map<String, Integer> heavyUser = [:]
		if (results?.size() == 2) {
			heavyUser.user = results[0]
			heavyUser.count = results[1]
		}

		return heavyUser
	}

    /**
     * Who has voted this restaurant the most
     */
	Map<User, Integer> heavyVoter() {
		List results = Vote.createCriteria().get {
			eq("restaurant", this)
			projections {
				round {
					groupProperty("user")
				}
				sum("vote", "voteSum")
			}
			maxResults(1)
			order("voteSum", "desc")
		}

		Map<String, Integer> heavyVoter = [:]
		if (results?.size() == 2) {
			heavyVoter.user = results[0]
			heavyVoter.sum = results[1]
		}

		return heavyVoter
	}

    /**
     * This calculates restaurant's average price
     */
    int avgPrice() {
        int avgPrice = Round.createCriteria().get {
            eq('selection', this)
            projections {
                avg('originalPrice')
            }
            maxResults(1)
        }

        return avgPrice ?: 0
    }

    /**
     * How much money has been used in this restaurant
     */
    int totalMoneyUsed() {
        int moneyUsed = Round.createCriteria().get {
            eq("selection", this)
            projections {
                sum("originalPrice")
            }
        }

        return moneyUsed ?: 0
    }

    /**
     * Number of used luncheon vouchers in this restaurant
     */
    int luncheonVoucherCount() {
        return Round.countBySelectionAndLuncheonVoucherUsed(this, true)
    }

    /**
     * How many votes this restaurant has received
     */
    int voteCount() {
        return Vote.countByRestaurant(this)
    }

    /**
     * How many selections this restaurant has received
     */
    int selectionCount() {
        return Round.countBySelection(this)
    }

    /**
     * How many votes one selection needs
     */
    float voteSelectionRatio() {
        int voteCount = voteCount()
        int selectionCount = selectionCount()

        if (selectionCount > 0) {
            return voteCount / selectionCount
        } else {
            return 0
        }
    }

    /**
     * Get a list of restaurants and how many votes one selection needs
     */
    static Map<Restaurant, Float> voteSelectionRatioList(String order = "desc", int max = 5) {
        Map<Restaurant, Float> results = [:]

        for(Restaurant r in Restaurant.list()) {
            results[r] = r.voteSelectionRatio()
        }

        if (order == "desc") {
            results = results.sort( { a, b -> b.value <=> a.value } )
        } else if (order == "asc") {
            results = results.sort( { it.value } )
        }

        Map<Restaurant, Float> orderedResults = [:]
        int i = 0
        results.each { key, value ->
            if (i < max) {
                orderedResults[key] = value
            }
        }

        return results
    }

    /**
     * Get a list of the most popular restaurants for each day
     */
    static Map<Day, Restaurant> popularByDay() {

        Map<Day, Restaurant> results = [:]

        // Days from sunday to saturday
        for (int i = 1; i <= 7; i++) {
            List daysFavourit = Round.executeQuery("SELECT new Map(r.selection as restaurant, COUNT(*) AS c) FROM Round r WHERE DAYOFWEEK(r.day) = :day AND r.selection IS NOT NULL GROUP BY r.selection", [day:i], [max:1])
            int ordinal = ((i - 2) >= 0) ? i - 2 : 6
            Day todayEnum = Day.values()[ordinal]
            results[todayEnum] = daysFavourit[0]?.restaurant
        }

        return results
    }

    /**
     * What is the most popular day for this restaurant
     */
    Day mostPopularDayOfWeek() {
        List results = Round.executeQuery("SELECT new Map(DAYOFWEEK(r.day) AS dayOfWeek, COUNT(r.selection) AS c) FROM Round r WHERE r.selection = :restaurant GROUP BY (DAYOFWEEK(r.day)) ORDER BY (COUNT(r.selection)) DESC", ["restaurant":this, max:1])

        if (results?.size() > 0) {
            int day = results[0].dayOfWeek
            int ordinal = ((day - 2) >= 0) ? day - 2 : 6
            Day mostPopularDay = Day.values()[ordinal]
            return mostPopularDay
        }
        return null
    }

    static List<Restaurant> orderedList(RestaurantSort sort = RestaurantSort.NAME, String order = "desc") {
        List<Restaurant> results = []
        Map<Restaurant, Integer> orderMap = [:]
        for (Restaurant r in Restaurant.list()) {
            int weight = getOrderingWeight(r, sort)
            orderMap[r] = weight
        }

        if (order == "desc") {
            orderMap = orderMap.sort { a, b -> b.value <=> a.value }
        } else {
            orderMap = orderMap.sort { it.value }
        }

        return orderMap.keySet() as List
    }

    private static int getOrderingWeight(Restaurant r, RestaurantSort sort) {
        switch (sort) {
            case RestaurantSort.VOTES:
                return r.voteCount()
            case RestaurantSort.SELECTIONS:
                return r.selectionCount()
            case RestaurantSort.PRICE:
                return r.avgPrice()
            case RestaurantSort.DISTANCE:
                return r.distance
            case RestaurantSort.NAME:
                return r.name
        }
    }
}